package com.autumn.platform.core.context.token.support;

import java.security.SecureRandom;
import java.util.Base64;
import java.util.Date;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import com.autumn.platform.core.context.token.Token;
import com.autumn.platform.core.context.token.TokenService;
import com.autumn.platform.core.context.token.codec.Hex;
import com.autumn.platform.core.context.token.codec.Utf8;

public class KeyBasedPersistenceTokenService implements TokenService, InitializingBean {
    private int pseudoRandomNumberBytes = 32;
    private String serverSecret;
    private Integer serverInteger;
    private SecureRandom secureRandom;

    @Override
    public Token allocateToken(String extendedInformation) {
        Assert.notNull(extendedInformation,
                "Must provided non-null extendedInformation (but it can be empty)");
        long creationTime = new Date().getTime();
        String serverSecret = computeServerSecretApplicableAt(creationTime);
        String pseudoRandomNumber = generatePseudoRandomNumber();
        String content = Long.toString(creationTime) + ":" + pseudoRandomNumber + ":"
                + extendedInformation;

        // Compute key
        String sha512Hex = Sha512DigestUtils.shaHex(content + ":" + serverSecret);
        String keyPayload = content + ":" + sha512Hex;
        String key = Utf8.decode(Base64.getEncoder().encode(Utf8.encode(keyPayload)));

        return new DefaultToken(key, creationTime, extendedInformation);
    }

    @Override
    public Token verifyToken(String key) {
        try {
            if (key == null || "".equals(key)) {
                return null;
            }
            String[] tokens = StringUtils.delimitedListToStringArray(
                    Utf8.decode(Base64.getDecoder().decode(Utf8.encode(key))), ":");
            Assert.isTrue(tokens.length >= 4, "Expected 4 or more tokens but found "
                    + tokens.length);

            long creationTime;
            try {
                creationTime = Long.decode(tokens[0]).longValue();
            } catch (NumberFormatException nfe) {
                throw new IllegalArgumentException("Expected number but found " + tokens[0]);
            }

            String serverSecret = computeServerSecretApplicableAt(creationTime);
            String pseudoRandomNumber = tokens[1];

            // Permit extendedInfo to itself contain ":" characters
            StringBuilder extendedInfo = new StringBuilder();
            for (int i = 2; i < tokens.length - 1; i++) {
                if (i > 2) {
                    extendedInfo.append(":");
                }
                extendedInfo.append(tokens[i]);
            }

            String sha1Hex = tokens[tokens.length - 1];

            // Verification
            String content = Long.toString(creationTime) + ":" + pseudoRandomNumber + ":"
                    + extendedInfo.toString();
            String expectedSha512Hex = Sha512DigestUtils.shaHex(content + ":" + serverSecret);
            Assert.isTrue(expectedSha512Hex.equals(sha1Hex), "Key verification failure");

            return new DefaultToken(key, creationTime, extendedInfo.toString());
        } catch (Exception e) {
            return null;
        }
    }

    private String generatePseudoRandomNumber() {
        byte[] randomBytes = new byte[pseudoRandomNumberBytes];
        secureRandom.nextBytes(randomBytes);
        return new String(Hex.encode(randomBytes));
    }

    private String computeServerSecretApplicableAt(long time) {
        return serverSecret + ":" + new Long(time % serverInteger.intValue()).intValue();
    }

    public void setServerSecret(String serverSecret) {
        this.serverSecret = serverSecret;
    }

    public void setSecureRandom(SecureRandom secureRandom) {
        this.secureRandom = secureRandom;
    }

    public void setPseudoRandomNumberBytes(int pseudoRandomNumberBytes) {
        Assert.isTrue(pseudoRandomNumberBytes >= 0,
                "Must have a positive pseudo random number bit size");
        this.pseudoRandomNumberBytes = pseudoRandomNumberBytes;
    }

    public void setServerInteger(Integer serverInteger) {
        this.serverInteger = serverInteger;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        Assert.hasText(serverSecret, "Server secret required");
        Assert.notNull(serverInteger, "Server integer required");
        Assert.notNull(secureRandom, "SecureRandom instance required");
    }
}
